// Copyright 2004-2007 Castle Project - http://www.castleproject.org/
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// MODIFICATIONS HAVE BEEN MADE TO THIS FILE
// COMDIV:
//  1. NEW: FileName property added to simplify resoving of subviews
//  2. NEW: Factory property added to simplify resolving of subviews
//  3. NEW: caching view render result
//  4. NEW: isempty(object) - intended to use with IEnumerable


using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Mvc;
using MvcContrib.ViewFactories;

#if MONORAIL
namespace Castle.MonoRail.Views.Brail
#else

namespace MvcContrib.Comdiv.ViewEngines.Brail
#endif
{
    /// <summary>
    ///Base class for all the view scripts, this is the class that is responsible for
    /// support all the behind the scenes magic such as variable to PropertyBag trasnlation, 
    /// resources usage, etc. 
    /// </summary>
    public abstract partial class BrailBase : IView, IViewDataContainer{
        protected IController __controller;

        /// <summary>
        /// This is used by layout scripts only, for outputing the child's content
        /// </summary>
        protected TextWriter childOutput;


//		protected IEngineContext context;
        public string LastVariableAccessed;
        private TextWriter outputStream;

        /// <summary>
        /// usually used by the layout to refer to its view, or a subview to its parent
        /// </summary>
        protected BrailBase parent;

        private Hashtable properties = new Hashtable(StringComparer.InvariantCultureIgnoreCase);

        /// <summary>
        /// used to hold the ComponentParams from the view, so their views/sections could access them
        /// </summary>
//		private IList viewComponentsParameters;
        protected BooViewEngine viewEngine;

        /// <summary>
        /// Initializes a new instance of the <see cref="BrailBase"/> class.
        /// </summary>
        /// <param name="viewEngine">The view engine.</param>
        /// <param name="output">The output.</param>
        public BrailBase(BooViewEngine viewEngine)
//		(BooViewEngine viewEngine, TextWriter output, IEngineContext context, IController __controller, IControllerContext __controllerContext)
        {
            this.viewEngine = viewEngine;
        }

        public BrailViewFactory Factory { get; set; }

        public BrailBase Layout { get; set; }

        ///// <summary>
        /////The path of the script, this is filled by AddBrailBaseClassStep
        ///// and is used for sub views 
        ///// </summary>
        //public virtual string ScriptDirectory
        //{
        //    get { return viewEngine.ViewRootDir; }
        //}

        public ViewContext ViewContext{
            get { return __viewContext; }
            set { __viewContext = value; }
        }

        /// <summary>
        /// Gets the view engine.
        /// </summary>
        /// <value>The view engine.</value>
        public BooViewEngine ViewEngine{
            get { return viewEngine; }
        }

        /// <summary>
        /// Gets the flash.
        /// </summary>
        /// <value>The flash.</value>
        public TempDataDictionary Flash{
            get { return __viewContext.TempData; }
        }

        /// <summary>
        /// Gets the output stream.
        /// </summary>
        /// <value>The output stream.</value>
        public TextWriter OutputStream{
            get { return _catchwriter ?? outputStream; }
        }



        /// <summary>
        /// Gets or sets the child output.
        /// </summary>
        /// <value>The child output.</value>
        public TextWriter ChildOutput{
            get { return childOutput; }
            set { childOutput = value; }
        }

        /// <summary>
        /// Gets the properties.
        /// </summary>
        /// <value>The properties.</value>
        public IDictionary Properties{
            get { return properties; }
        }

        #region IView Members

        public void Render(ViewContext viewContext, TextWriter writer){
            __controller = viewContext.Controller;
            __viewContext = viewContext;

            if (Layout != null){
                Layout.ChildOutput = outputStream = new StringWriter();
            }
            else{
                outputStream = writer;
            }

            InitProperties(__viewContext);
            CacheIndependedWriter = outputStream;
            if (!readCache()){
                _render();
                writeToCache();
            }


            if (Layout != null){
                Layout.SetParent(this);
                try{
                    Layout.Render(__viewContext, writer);
                }
                catch (Exception e){
                    HandleException("", Layout, e);
                }
            }
        }

        #endregion

        #region IViewDataContainer Members

        public ViewDataDictionary ViewData{
            get { return ViewContext.ViewData; }
            set { throw new NotSupportedException(); }
        }

        #endregion

        private void _render(){
            try{
                Run();
            }
            catch (Exception e){
                HandleException("", this, e);
            }
        }

        /// <summary>
        /// Runs this instance, this is generated by the script
        /// </summary>
        public abstract void Run();

        /// <summary>
        /// Output the subview to the client, this is either a relative path "SubView" which
        /// is relative to the current /script/ or an "absolute" path "/home/menu" which is
        /// actually relative to ViewDirRoot
        /// </summary>
        /// <param name="subviewName"></param>
        public string OutputSubView(string subviewName){
            return OutputSubView(subviewName, new Hashtable());
        }

        /// <summary>
        /// Similiar to the OutputSubView(string) function, but with a bunch of parameters that are used
        /// just for this subview. This parameters are /not/ inheritable.
        /// </summary>
        /// <returns>An empty string, just to make it possible to use inline ${OutputSubView("foo")}</returns>
        public string OutputSubView(string subviewName, IDictionary parameters){
            OutputSubView(subviewName, OutputStream, parameters);
            return string.Empty;
        }

        /// <summary>
        /// Outputs the sub view to the writer
        /// </summary>
        /// <param name="subviewName">Name of the subview.</param>
        /// <param name="writer">The writer.</param>
        /// <param name="parameters">The parameters.</param>
        public void OutputSubView(string subviewName, TextWriter writer, IDictionary parameters){
            string subViewFileName = viewEngine.Factory.ResolveViewName(FileName, subviewName + ".brail");
            BrailBase subView = viewEngine.GetCompiledScriptInstance(subViewFileName);
            subView.SetParent(this);
            foreach (DictionaryEntry entry in parameters){
                subView.properties[entry.Key] = entry.Value;
            }
            subView.Render(__viewContext, writer);
            subView.BindExportedProperties();
        }

        /// <summary>
        /// this is called by ReplaceUnknownWithParameters step to create a more dynamic experiance
        /// any uknown identifier will be translate into a call for GetParameter('identifier name').
        /// This mean that when an uknonwn identifier is in the script, it will only be found on runtime.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public object GetParameter(string name){
            ParameterSearch search = GetParameterInternal(name);
            if (search.Found == false){
                throw new Exception("Parameter '" + name + "' was not found!");
            }
            return search.Value;
        }

        /// <summary>
        /// this is called by ReplaceUnknownWithParameters step to create a more dynamic experiance
        /// any uknown identifier with the prefix of ? will be translated into a call for 
        /// TryGetParameter('identifier name without the ? prefix').
        /// This method will return null if the value it not found.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public object TryGetParameter(string name){
            ParameterSearch search = GetParameterInternal(name);
            return new IgnoreNull(search.Value);
        }

        /// <summary>
        /// Gets the parameter - implements the logic for searching parameters.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        private ParameterSearch GetParameterInternal(string name){
            LastVariableAccessed = name;
            //temporary syntax to turn @variable to varaible, imitating :symbol in ruby
            if (name.StartsWith("@")){
                return new ParameterSearch(name.Substring(1), true);
            }
            if (properties.Contains(name)){
                return new ParameterSearch(properties[name], true);
            }
            if (parent != null){
                return parent.GetParameterInternal(name);
            }
            return new ParameterSearch(null, false);
        }

        /// <summary>
        /// Sets the parent.
        /// </summary>
        /// <param name="myParent">My parent.</param>
        public void SetParent(BrailBase myParent){
            parent = myParent;
        }

        /// <summary>
        /// Allows to check that a parameter was defined
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public bool IsDefined(string name){
            ParameterSearch search = GetParameterInternal(name);
            return search.Found;
        }

        /// <summary>
        /// Will output the given value as escaped HTML
        /// </summary>
        /// <param name="toOutput"></param>
        public void OutputEscaped(object toOutput){
            if (toOutput == null){
                return;
            }
            string str = toOutput.ToString();
            OutputStream.Write(HttpUtility.HtmlEncode(str));
        }

        /// <summary>
        /// Note that this will overwrite any existing property.
        /// </summary>
        public void AddProperty(string name, object item){
            properties[name] = item;
        }


        private void InitProperties(ViewContext viewContext){
            //properties.Add("dsl", new DslWrapper(this));
            properties.Add("Controller", viewContext.Controller);

            HttpContextBase myContext = viewContext.HttpContext;

            //in standalone or test call we uses EmptyHttpContext which is not public, but here we must avoid it
            if (myContext.GetType().Name == "EmptyHttpContext"){
                myContext = null;
            }

            properties.Add("Context", myContext);
            if (myContext != null){
                properties.Add("request", myContext.Request);
                properties.Add("response", myContext.Response);
                properties.Add("session", myContext.Session);
            }

            properties["html"] = new HtmlHelper(viewContext, this);
            properties["url"] = new UrlHelper(viewContext.RequestContext);

            if (myContext != null && myContext.Request.QueryString != null){
                foreach (string key in myContext.Request.QueryString.AllKeys){
                    if (key == null){
                        continue;
                    }
                    properties[key] = myContext.Request.QueryString[key];
                }
            }

            if (myContext != null && myContext.Request.Form != null){
                foreach (string key in myContext.Request.Form.AllKeys){
                    if (key == null){
                        continue;
                    }
                    properties[key] = myContext.Request.Form[key];
                }
            }

            if (viewContext.TempData != null){
                foreach (var entry in viewContext.TempData){
                    properties[entry.Key] = entry.Value;
                }
            }
            if (viewContext.ViewData != null){
                foreach (var entry in viewContext.ViewData){
                    if (!properties.ContainsKey(entry.Key)){
                        properties[entry.Key] = entry.Value;
                    }
                }
                properties["viewData"] = viewContext.ViewData;
            }


            if (myContext != null){
                string siteRoot = myContext.Request.ApplicationPath;
                if (siteRoot != null && !siteRoot.EndsWith("/")){
                    siteRoot += "/";
                }

                properties["siteRoot"] = siteRoot;
            }
        }

        private void HandleException(string templateName, BrailBase view, Exception e){
            var sb = new StringBuilder();
            sb.Append("Exception on RenderView: ").AppendLine(templateName);
            sb.Append("Last accessed variable: ").Append(view.LastVariableAccessed);
            string msg = sb.ToString();
            sb.Append("Exception: ").AppendLine(e.ToString());
//			Log(msg);
            throw new Exception(msg, e);
        }

        #region Nested type: ParameterSearch

        private class ParameterSearch{
            private readonly bool found;
            private readonly object value;

            public ParameterSearch(object value, bool found){
                this.found = found;
                this.value = value;
            }

            public bool Found{
                get { return found; }
            }

            public object Value{
                get { return value; }
            }
        }

        #endregion
    }
}